Skip to content

Add MA0186: Report missing nullable attributes on Equals and TryGetValue parameters#998

Merged
meziantou merged 9 commits into
mainfrom
copilot/introduce-missing-nullable-rule
Feb 5, 2026
Merged

Add MA0186: Report missing nullable attributes on Equals and TryGetValue parameters#998
meziantou merged 9 commits into
mainfrom
copilot/introduce-missing-nullable-rule

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 4, 2026

  • Add new rule identifier MA0186 for missing nullable attributes
  • Create analyzer for detecting missing nullable attributes on method parameters
    • Check Equals(object?) override with [NotNullWhen(true)]
    • Check IEquatable.Equals(T?) implementation with [NotNullWhen(true)]
    • Check IDictionary<TKey, TValue>.TryGetValue with [MaybeNullWhen(false)]
    • Skip value types where T is a value type
    • Use helpers for interface implementation checks
    • Check base member for overrides
    • Ensure nullable annotations are enabled
  • Create tests for the analyzer
  • Run tests to validate implementation
  • Update documentation by running DocumentationGenerator
  • Add detailed documentation with examples
  • Verify build and tests pass
  • Regenerate documentation with source links

Implementation

This rule detects two patterns of missing nullable attributes:

  1. Equals methods: Reports missing [NotNullWhen(true)] on parameters of Equals(object?) overrides and IEquatable<T>.Equals(T?) implementations
  2. TryGetValue methods: Reports missing [MaybeNullWhen(false)] on the value parameter of IDictionary<TKey, TValue>.TryGetValue implementations

Both patterns improve nullable reference type analysis by helping the compiler understand nullability flow based on method return values.

Configuration

  • Rule ID: MA0186
  • Category: Design
  • Severity: Info (suggestion)
  • Enabled by default: No
  • Has code fix: No
Original prompt

This section details on the original issue you should resolve

<issue_title>Introduce rule that reports missing nullable attributes</issue_title>
<issue_description>I'm sure there are other examples, but one case where it would be interesting to report missing nullable attributes is for the lack of [NotNullWhen(true)] on the Equals(object?) override and the implementation of IEquatable<T>:

public sealed class Foo : IEquatable<Foo>
{
    public override bool Equals([NotNullWhen(true)] object? obj)
    {
        ...
    }

    public bool Equals([NotNullWhen(true)] Foo? other)
    {
        ...
    }
}

The rule should of course also support the explicit interface implementation of IEquatable<T> and only report a diagnostic if NRT is enabled for the method in question.

It should not be reported for the IEquatable<T> implementation if T is a value type.

</issue_description>

<agent_instructions>Can you implement this rule as a suggestion, but keep it disabled by default</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits February 4, 2026 20:30
Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copilot AI changed the title [WIP] Introduce rule that reports missing nullable attributes Add MA0186: Report missing [NotNullWhen(true)] on Equals parameters Feb 4, 2026
Copilot AI requested a review from meziantou February 4, 2026 20:38
@meziantou
Copy link
Copy Markdown
Owner

@copilot Can you regenerate the documentation?

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 4, 2026

@copilot Can you regenerate the documentation?

Done in commit 91d0c1f. The documentation now includes the source link to the analyzer file.

@meziantou
Copy link
Copy Markdown
Owner

@copilot the original issue also ask for something about idictionary #975 (comment)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 4, 2026

@copilot the original issue also ask for something about idictionary #975 (comment)

I've implemented support for:

  1. Equals(object?) override
  2. IEquatable<T>.Equals(T?) public implementation
  3. ✅ Skipping value types (where T in IEquatable is a value type)

I'm having difficulty with explicit interface implementations (bool IEquatable<T>.Equals(T? other)) - the Roslyn symbol analysis doesn't seem to pick them up the same way. Could you clarify what you mean by "something about idictionary"? I only see the Equals/IEquatable requirements in the original issue. Is there another method pattern (like TryGetValue) that should also be checked?

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/meziantou/Meziantou.Analyzer/issues/975
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (http block)
  • pdfvsblobprodcus380.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • st8vsblobprodcus339.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

- Skip reporting diagnostic for IEquatable<T>.Equals when T is a value type
- Simplify IEquatable detection logic to work for both public and explicit implementations
- Add test for value type scenario

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Comment thread src/Meziantou.Analyzer/Rules/MissingNotNullWhenAttributeOnEqualsAnalyzer.cs Outdated
Comment thread src/Meziantou.Analyzer/Rules/MissingNotNullWhenAttributeOnEqualsAnalyzer.cs Outdated
- Use IsInterfaceImplementation() and GetImplementingInterfaceSymbol() helpers
- Check OverriddenMethod chain to verify object.Equals override
- Add tests for #nullable enable/disable scenarios
- NullableAnnotation check already ensures nullable is enabled

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 5, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 1s1vsblobprodcus386.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • pdfvsblobprodcus380.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • st8vsblobprodcus339.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@meziantou
Copy link
Copy Markdown
Owner

@copilot The original issues mentioned another method to handle

Another - much less common - example would be missing [MaybeNullWhen(false)] on value argument of an implementation of IDictionary<TKey,TValue>.TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value).

- Detect missing [MaybeNullWhen(false)] on value parameter of IDictionary<TKey, TValue>.TryGetValue implementations
- Add tests for TryGetValue scenarios
- Update documentation to cover both Equals and TryGetValue patterns

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 5, 2026

@copilot The original issues mentioned another method to handle

Another - much less common - example would be missing [MaybeNullWhen(false)] on value argument of an implementation of IDictionary<TK...

Done in commit 9ee4a9f. Now detecting missing [MaybeNullWhen(false)] on the value parameter of IDictionary<TKey, TValue>.TryGetValue implementations. Added tests and updated documentation to cover both patterns.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 1s1vsblobprodcus386.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • pdfvsblobprodcus380.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • s4uvsblobprodcus326.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
  • st8vsblobprodcus339.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/Meziantou.Analyzer.slnx --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)
    • Triggering command: /usr/share/dotnet/dotnet dotnet restore --no-dependencies /home/REDACTED/work/Meziantou.Analyzer/Meziantou.Analyzer/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj --packages /home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/home/REDACTED/work/Meziantou.Analyzer/.codeql-scratch/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

This was referenced May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce rule that reports missing nullable attributes

2 participants